package cn.hutool.http;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.resource.BytesResource;
import cn.hutool.core.io.resource.FileResource;
import cn.hutool.core.io.resource.MultiFileResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.SSLUtil;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.body.FormUrlEncodedBody;
import cn.hutool.http.body.MultipartBody;
import cn.hutool.http.body.RequestBody;
import cn.hutool.http.body.ResourceBody;
import cn.hutool.http.cookie.GlobalCookieManager;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * http请求类<br>
 * Http请求类用于构建Http请求并同步获取结果，此类通过CookieManager持有域名对应的Cookie值，再次请求时会自动附带Cookie信息
 *
 * @author Looly
 */
public class HttpRequest extends HttpBase<HttpRequest> {

    // ---------------------------------------------------------------- static Http Method start

    /**
     * POST请求
     *
     * @param url URL
     * @return HttpRequest
     */
    public static HttpRequest post(String url) {
        return of(url).method(Method.POST);
    }

    /**
     * GET请求
     *
     * @param url URL
     * @return HttpRequest
     */
    public static HttpRequest get(String url) {
        return of(url).method(Method.GET);
    }

    /**
     * HEAD请求
     *
     * @param url URL
     * @return HttpRequest
     */
    public static HttpRequest head(String url) {
        return of(url).method(Method.HEAD);
    }

    /**
     * OPTIONS请求
     *
     * @param url URL
     * @return HttpRequest
     */
    public static HttpRequest options(String url) {
        return of(url).method(Method.OPTIONS);
    }

    /**
     * PUT请求
     *
     * @param url URL
     * @return HttpRequest
     */
    public static HttpRequest put(String url) {
        return of(url).method(Method.PUT);
    }

    /**
     * PATCH请求
     *
     * @param url URL
     * @return HttpRequest
     * @since 3.0.9
     */
    public static HttpRequest patch(String url) {
        return of(url).method(Method.PATCH);
    }

    /**
     * DELETE请求
     *
     * @param url URL
     * @return HttpRequest
     */
    public static HttpRequest delete(String url) {
        return of(url).method(Method.DELETE);
    }

    /**
     * TRACE请求
     *
     * @param url URL
     * @return HttpRequest
     */
    public static HttpRequest trace(String url) {
        return of(url).method(Method.TRACE);
    }

    /**
     * 构建一个HTTP请求<br>
     * 对于传入的URL，可以自定义是否解码已经编码的内容，设置见{@link HttpGlobalConfig#setDecodeUrl(boolean)}<br>
     * 在构建Http请求时，用户传入的URL可能有编码后和未编码的内容混合在一起，如果{@link HttpGlobalConfig#isDecodeUrl()}为{@code true}，则会统一解码编码后的参数，<br>
     * 按照RFC3986规范，在发送请求时，全部编码之。如果为{@code false}，则不会解码已经编码的内容，在请求时只编码需要编码的部分。
     *
     * @param url URL链接，默认自动编码URL中的参数等信息
     * @return HttpRequest
     * @since 5.7.18
     */
    public static HttpRequest of(String url) {
        return of(url, HttpGlobalConfig.isDecodeUrl() ? DEFAULT_CHARSET : null);
    }

    /**
     * 构建一个HTTP请求<br>
     * 对于传入的URL，可以自定义是否解码已经编码的内容。<br>
     * 在构建Http请求时，用户传入的URL可能有编码后和未编码的内容混合在一起，如果charset参数不为{@code null}，则会统一解码编码后的参数，<br>
     * 按照RFC3986规范，在发送请求时，全部编码之。如果为{@code false}，则不会解码已经编码的内容，在请求时只编码需要编码的部分。
     *
     * @param url     URL链接
     * @param charset 编码，如果为{@code null}不自动解码编码URL
     * @return HttpRequest
     * @since 5.7.18
     */
    public static HttpRequest of(String url, Charset charset) {
        return of(UrlBuilder.ofHttp(url, charset));
    }

    /**
     * 构建一个HTTP请求<br>
     *
     * @param url {@link UrlBuilder}
     * @return HttpRequest
     * @since 5.8.0
     */
    public static HttpRequest of(UrlBuilder url) {
        return new HttpRequest(url);
    }

    /**
     * 设置全局默认的连接和读取超时时长
     *
     * @param customTimeout 超时时长
     * @see HttpGlobalConfig#setTimeout(int)
     * @since 4.6.2
     */
    public static void setGlobalTimeout(int customTimeout) {
        HttpGlobalConfig.setTimeout(customTimeout);
    }

    /**
     * 获取Cookie管理器，用于自定义Cookie管理
     *
     * @return {@link CookieManager}
     * @see GlobalCookieManager#getCookieManager()
     * @since 4.1.0
     */
    public static CookieManager getCookieManager() {
        return GlobalCookieManager.getCookieManager();
    }

    /**
     * 自定义{@link CookieManager}
     *
     * @param customCookieManager 自定义的{@link CookieManager}
     * @see GlobalCookieManager#setCookieManager(CookieManager)
     * @since 4.5.14
     */
    public static void setCookieManager(CookieManager customCookieManager) {
        GlobalCookieManager.setCookieManager(customCookieManager);
    }

    /**
     * 关闭Cookie
     *
     * @see GlobalCookieManager#setCookieManager(CookieManager)
     * @since 4.1.9
     */
    public static void closeCookie() {
        GlobalCookieManager.setCookieManager(null);
    }
    // ---------------------------------------------------------------- static Http Method end

    private HttpConfig config = HttpConfig.create();
    private UrlBuilder url;
    private URLStreamHandler urlHandler;
    private Method method = Method.GET;
    /**
     * 连接对象
     */
    private HttpConnection httpConnection;

    /**
     * 存储表单数据
     */
    private Map<String, Object> form;
    /**
     * Cookie
     */
    private String cookie;
    /**
     * 是否为Multipart表单
     */
    private boolean isMultiPart;
    /**
     * 是否是REST请求模式
     */
    private boolean isRest;
    /**
     * 重定向次数计数器，内部使用
     */
    private int redirectCount;
    /**
     * 固定长度，用于设置HttpURLConnection.setFixedLengthStreamingMode，默认为0，表示使用默认值，默认值由HttpURLConnection内部决定，通常为0
     */
    private long fixedContentLength;

    /**
     * 构造，URL编码默认使用UTF-8
     *
     * @param url URL
     * @deprecated 请使用 {@link #of(String)}
     */
    @Deprecated
    public HttpRequest(String url) {
        this(UrlBuilder.ofHttp(url));
    }

    /**
     * 构造
     *
     * @param url {@link UrlBuilder}
     */
    public HttpRequest(UrlBuilder url) {
        this.url = Assert.notNull(url, "URL must be not null!");
        // 给定默认URL编码
        final Charset charset = url.getCharset();
        if (null != charset) {
            this.charset(charset);
        }
        // 给定一个默认头信息
        this.header(GlobalHeaders.INSTANCE.headers);
    }

    /**
     * 获取请求URL
     *
     * @return URL字符串
     * @since 4.1.8
     */
    public String getUrl() {
        return url.toString();
    }

    /**
     * 设置URL
     *
     * @param url url字符串
     * @return this
     * @since 4.1.8
     */
    public HttpRequest setUrl(String url) {
        return setUrl(UrlBuilder.ofHttp(url, this.charset));
    }

    /**
     * 设置URL
     *
     * @param urlBuilder url字符串
     * @return this
     * @since 5.3.1
     */
    public HttpRequest setUrl(UrlBuilder urlBuilder) {
        this.url = urlBuilder;
        return this;
    }

    /**
     * 设置{@link URLStreamHandler}
     * <p>
     * 部分环境下需要单独设置此项，例如当 WebLogic Server 实例充当 SSL 客户端角色（它会尝试通过 SSL 连接到其他服务器或应用程序）时，<br>
     * 它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。如果主机名不匹配，则删除此连接。<br>
     * 因此weblogic不支持https的sni协议的主机名验证，此时需要将此值设置为sun.net.www.protocol.https.Handler对象。
     * <p>
     * 相关issue见：<a href="https://gitee.com/dromara/hutool/issues/IMD1X">https://gitee.com/dromara/hutool/issues/IMD1X</a>
     *
     * @param urlHandler {@link URLStreamHandler}
     * @return this
     * @since 4.1.9
     */
    public HttpRequest setUrlHandler(URLStreamHandler urlHandler) {
        this.urlHandler = urlHandler;
        return this;
    }

    /**
     * 获取Http请求方法
     *
     * @return {@link Method}
     * @since 4.1.8
     */
    public Method getMethod() {
        return this.method;
    }

    /**
     * 设置请求方法
     *
     * @param method HTTP方法
     * @return HttpRequest
     * @see #method(Method)
     * @since 4.1.8
     */
    public HttpRequest setMethod(Method method) {
        return method(method);
    }

    /**
     * 获取{@link HttpConnection}<br>
     * 在{@link #execute()} 执行前此对象为null
     *
     * @return {@link HttpConnection}
     * @since 4.2.2
     */
    public HttpConnection getConnection() {
        return this.httpConnection;
    }

    /**
     * 设置固定长度的流模式，会设置HTTP请求头中的Content-Length字段，告知服务器整个请求体的精确字节大小。<br>
     * 这在上传文件或大数据量时非常有用，因为它允许服务器准确地知道何时接收完所有的请求数据，而不需要依赖于连接的关闭来判断数据传输的结束。
     *
     * @param contentLength 固定长度
     * @return this
     * @since 5.8.33
     */
    public HttpRequest setFixedContentLength(long contentLength) {
        this.fixedContentLength = contentLength;
        return this;
    }

    /**
     * 设置请求方法
     *
     * @param method HTTP方法
     * @return HttpRequest
     */
    public HttpRequest method(Method method) {
        this.method = method;
        return this;
    }

    // ---------------------------------------------------------------- Http Request Header start

    /**
     * 设置contentType
     *
     * @param contentType contentType
     * @return HttpRequest
     */
    public HttpRequest contentType(String contentType) {
        header(Header.CONTENT_TYPE, contentType);
        return this;
    }

    /**
     * 设置是否为长连接
     *
     * @param isKeepAlive 是否长连接
     * @return HttpRequest
     */
    public HttpRequest keepAlive(boolean isKeepAlive) {
        header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
        return this;
    }

    /**
     * @return 获取是否为长连接
     */
    public boolean isKeepAlive() {
        String connection = header(Header.CONNECTION);
        if (connection == null) {
            return false == HTTP_1_0.equalsIgnoreCase(httpVersion);
        }

        return false == "close".equalsIgnoreCase(connection);
    }

    /**
     * 获取内容长度
     *
     * @return String
     */
    public String contentLength() {
        return header(Header.CONTENT_LENGTH);
    }

    /**
     * 设置内容长度
     *
     * @param value 长度
     * @return HttpRequest
     */
    public HttpRequest contentLength(int value) {
        header(Header.CONTENT_LENGTH, String.valueOf(value));
        return this;
    }

    /**
     * 设置Cookie<br>
     * 自定义Cookie后会覆盖Hutool的默认Cookie行为
     *
     * @param cookies Cookie值数组，如果为{@code null}则设置无效，使用默认Cookie行为
     * @return this
     * @since 5.4.1
     */
    public HttpRequest cookie(Collection<HttpCookie> cookies) {
        return cookie(CollUtil.isEmpty(cookies) ? null : cookies.toArray(new HttpCookie[0]));
    }

    /**
     * 设置Cookie<br>
     * 自定义Cookie后会覆盖Hutool的默认Cookie行为
     *
     * @param cookies Cookie值数组，如果为{@code null}则设置无效，使用默认Cookie行为
     * @return this
     * @since 3.1.1
     */
    public HttpRequest cookie(HttpCookie... cookies) {
        if (ArrayUtil.isEmpty(cookies)) {
            return disableCookie();
        }
        // 名称/值对之间用分号和空格 ('; ')
        // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cookie
        return cookie(ArrayUtil.join(cookies, "; "));
    }

    /**
     * 设置Cookie<br>
     * 自定义Cookie后会覆盖Hutool的默认Cookie行为
     *
     * @param cookie Cookie值，如果为{@code null}则设置无效，使用默认Cookie行为
     * @return this
     * @since 3.0.7
     */
    public HttpRequest cookie(String cookie) {
        this.cookie = cookie;
        return this;
    }

    /**
     * 禁用默认Cookie行为，此方法调用后会将Cookie置为空。<br>
     * 如果想重新启用Cookie，请调用：{@link #cookie(String)}方法自定义Cookie。<br>
     * 如果想启动默认的Cookie行为（自动回填服务器传回的Cookie），则调用{@link #enableDefaultCookie()}
     *
     * @return this
     * @since 3.0.7
     */
    public HttpRequest disableCookie() {
        return cookie(StrUtil.EMPTY);
    }

    /**
     * 打开默认的Cookie行为（自动回填服务器传回的Cookie）
     *
     * @return this
     */
    public HttpRequest enableDefaultCookie() {
        return cookie((String) null);
    }
    // ---------------------------------------------------------------- Http Request Header end

    // ---------------------------------------------------------------- Form start

    /**
     * 设置表单数据<br>
     *
     * @param name  名
     * @param value 值
     * @return this
     */
    public HttpRequest form(String name, Object value) {
        if (StrUtil.isBlank(name) || ObjectUtil.isNull(value)) {
            return this; // 忽略非法的form表单项内容;
        }

        // 停用body
        this.body = null;

        if (value instanceof File) {
            // 文件上传
            return this.form(name, (File) value);
        }

        if (value instanceof Resource) {
            return form(name, (Resource) value);
        }

        // 普通值
        String strValue;
        if (value instanceof Iterable) {
            // 列表对象
            strValue = CollUtil.join((Iterable<?>) value, ",");
        } else if (ArrayUtil.isArray(value)) {
            if (File.class == ArrayUtil.getComponentType(value)) {
                // 多文件
                return this.form(name, (File[]) value);
            }
            // 数组对象
            strValue = ArrayUtil.join((Object[]) value, ",");
        } else {
            // 其他对象一律转换为字符串
            strValue = Convert.toStr(value, null);
        }

        return putToForm(name, strValue);
    }

    /**
     * 设置表单数据
     *
     * @param name       名
     * @param value      值
     * @param parameters 参数对，奇数为名，偶数为值
     * @return this
     */
    public HttpRequest form(String name, Object value, Object... parameters) {
        form(name, value);

        for (int i = 0; i < parameters.length; i += 2) {
            form(parameters[i].toString(), parameters[i + 1]);
        }
        return this;
    }

    /**
     * 设置map类型表单数据
     *
     * @param formMap 表单内容
     * @return this
     */
    public HttpRequest form(Map<String, Object> formMap) {
        if (MapUtil.isNotEmpty(formMap)) {
            formMap.forEach(this::form);
        }
        return this;
    }

    /**
     * 设置map&lt;String, String&gt;类型表单数据
     *
     * @param formMapStr 表单内容
     * @return this
     * @since 5.6.7
     */
    public HttpRequest formStr(Map<String, String> formMapStr) {
        if (MapUtil.isNotEmpty(formMapStr)) {
            formMapStr.forEach(this::form);
        }
        return this;
    }

    /**
     * 文件表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name  名
     * @param files 需要上传的文件，为空跳过
     * @return this
     */
    public HttpRequest form(String name, File... files) {
        if (ArrayUtil.isEmpty(files)) {
            return this;
        }
        if (1 == files.length) {
            final File file = files[0];
            return form(name, file, file.getName());
        }
        return form(name, new MultiFileResource(files));
    }

    /**
     * 文件表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name 名
     * @param file 需要上传的文件
     * @return this
     */
    public HttpRequest form(String name, File file) {
        return form(name, file, file.getName());
    }

    /**
     * 文件表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name     名
     * @param file     需要上传的文件
     * @param fileName 文件名，为空使用文件默认的文件名
     * @return this
     */
    public HttpRequest form(String name, File file, String fileName) {
        if (null != file) {
            form(name, new FileResource(file, fileName));
        }
        return this;
    }

    /**
     * 文件byte[]表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name      名
     * @param fileBytes 需要上传的文件
     * @param fileName  文件名
     * @return this
     * @since 4.1.0
     */
    public HttpRequest form(String name, byte[] fileBytes, String fileName) {
        if (null != fileBytes) {
            form(name, new BytesResource(fileBytes, fileName));
        }
        return this;
    }

    /**
     * 文件表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name     名
     * @param resource 数据源，文件可以使用{@link FileResource}包装使用
     * @return this
     * @since 4.0.9
     */
    public HttpRequest form(String name, Resource resource) {
        if (null != resource) {
            if (false == isKeepAlive()) {
                keepAlive(true);
            }

            this.isMultiPart = true;
            return putToForm(name, resource);
        }
        return this;
    }

    /**
     * 获取表单数据
     *
     * @return 表单Map
     */
    public Map<String, Object> form() {
        return this.form;
    }

    /**
     * 获取文件表单数据
     *
     * @return 文件表单Map
     * @since 3.3.0
     */
    public Map<String, Resource> fileForm() {
        final Map<String, Resource> result = MapUtil.newHashMap();
        this.form.forEach((key, value) -> {
            if (value instanceof Resource) {
                result.put(key, (Resource) value);
            }
        });
        return result;
    }
    // ---------------------------------------------------------------- Form end

    // ---------------------------------------------------------------- Body start

    /**
     * 设置内容主体<br>
     * 请求体body参数支持两种类型：
     *
     * <pre>
     * 1. 标准参数，例如 a=1&amp;b=2 这种格式
     * 2. Rest模式，此时body需要传入一个JSON或者XML字符串，Hutool会自动绑定其对应的Content-Type
     * </pre>
     *
     * @param body 请求体
     * @return this
     */
    public HttpRequest body(String body) {
        return this.body(body, null);
    }

    /**
     * 设置内容主体<br>
     * 请求体body参数支持两种类型：
     *
     * <pre>
     * 1. 标准参数，例如 a=1&amp;b=2 这种格式
     * 2. Rest模式，此时body需要传入一个JSON或者XML字符串，Hutool会自动绑定其对应的Content-Type
     * </pre>
     *
     * @param body        请求体
     * @param contentType 请求体类型，{@code null}表示自动判断类型
     * @return this
     */
    public HttpRequest body(String body, String contentType) {
        byte[] bytes = StrUtil.bytes(body, this.charset);
        body(bytes);
        this.form = null; // 当使用body时，停止form的使用

        if (null != contentType) {
            // Content-Type自定义设置
            this.contentType(contentType);
        } else {
            // 在用户未自定义的情况下自动根据内容判断
            contentType = HttpUtil.getContentTypeByRequestBody(body);
            if (null != contentType && ContentType.isDefault(this.header(Header.CONTENT_TYPE))) {
                if (null != this.charset) {
                    // 附加编码信息
                    contentType = ContentType.build(contentType, this.charset);
                }
                this.contentType(contentType);
            }
        }

        // 判断是否为rest请求
        if (StrUtil.containsAnyIgnoreCase(contentType, "json", "xml")) {
            this.isRest = true;
            contentLength(bytes.length);
        }
        return this;
    }

    /**
     * 设置主体字节码<br>
     * 需在此方法调用前使用charset方法设置编码，否则使用默认编码UTF-8
     *
     * @param bodyBytes 主体
     * @return this
     */
    public HttpRequest body(byte[] bodyBytes) {
        if (ArrayUtil.isNotEmpty(bodyBytes)) {
            return body(new BytesResource(bodyBytes));
        }
        return this;
    }

    /**
     * 设置主体字节码<br>
     * 需在此方法调用前使用charset方法设置编码，否则使用默认编码UTF-8
     *
     * @param resource 主体
     * @return this
     */
    public HttpRequest body(Resource resource) {
        if (null != resource) {
            this.body = resource;
        }
        return this;
    }
    // ---------------------------------------------------------------- Body end

    /**
     * 将新的配置加入<br>
     * 注意加入的配置可能被修改
     *
     * @param config 配置
     * @return this
     */
    public HttpRequest setConfig(HttpConfig config) {
        this.config = config;
        return this;
    }

    /**
     * 设置超时，单位：毫秒<br>
     * 超时包括：
     *
     * <pre>
     * 1. 连接超时
     * 2. 读取响应超时
     * </pre>
     *
     * @param milliseconds 超时毫秒数
     * @return this
     * @see #setConnectionTimeout(int)
     * @see #setReadTimeout(int)
     */
    public HttpRequest timeout(int milliseconds) {
        config.timeout(milliseconds);
        return this;
    }

    /**
     * 设置连接超时，单位：毫秒
     *
     * @param milliseconds 超时毫秒数
     * @return this
     * @since 4.5.6
     */
    public HttpRequest setConnectionTimeout(int milliseconds) {
        config.setConnectionTimeout(milliseconds);
        return this;
    }

    /**
     * 设置连接超时，单位：毫秒
     *
     * @param milliseconds 超时毫秒数
     * @return this
     * @since 4.5.6
     */
    public HttpRequest setReadTimeout(int milliseconds) {
        config.setReadTimeout(milliseconds);
        return this;
    }

    /**
     * 禁用缓存
     *
     * @return this
     */
    public HttpRequest disableCache() {
        config.disableCache();
        return this;
    }

    /**
     * 设置是否打开重定向，如果打开默认重定向次数为2<br>
     * 此方法效果与{@link #setMaxRedirectCount(int)} 一致
     *
     * <p>
     * 需要注意的是，当设置为{@code true}时，如果全局重定向次数非0，直接复用，否则设置默认2次。<br>
     * 当设置为{@code false}时，无论全局是否设置次数，都设置为0。<br>
     * 不调用此方法的情况下，使用全局默认的次数。
     * </p>
     *
     * @param isFollowRedirects 是否打开重定向
     * @return this
     */
    public HttpRequest setFollowRedirects(boolean isFollowRedirects) {
        if (isFollowRedirects) {
            if (config.maxRedirectCount <= 0) {
                // 默认两次跳转
                return setMaxRedirectCount(2);
            }
        } else {
            // 手动强制关闭重定向，此时不受全局重定向设置影响
            if (config.maxRedirectCount < 0) {
                return setMaxRedirectCount(0);
            }
        }
        return this;
    }

    /**
     * 自动重定向时是否处理cookie
     *
     * @param followRedirectsCookie 自动重定向时是否处理cookie
     * @return this
     */
    public HttpRequest setFollowRedirectsCookie(boolean followRedirectsCookie) {
        config.setFollowRedirectsCookie(followRedirectsCookie);
        return this;
    }

    /**
     * 设置最大重定向次数<br>
     * 如果次数小于1则表示不重定向，大于等于1表示打开重定向
     *
     * @param maxRedirectCount 最大重定向次数
     * @return this
     * @since 3.3.0
     */
    public HttpRequest setMaxRedirectCount(int maxRedirectCount) {
        config.setMaxRedirectCount(maxRedirectCount);
        return this;
    }

    /**
     * 设置域名验证器<br>
     * 只针对HTTPS请求，如果不设置，不做验证，所有域名被信任
     *
     * @param hostnameVerifier HostnameVerifier
     * @return this
     */
    public HttpRequest setHostnameVerifier(HostnameVerifier hostnameVerifier) {
        config.setHostnameVerifier(hostnameVerifier);
        return this;
    }

    /**
     * 设置Http代理
     *
     * @param host 代理 主机
     * @param port 代理 端口
     * @return this
     * @since 5.4.5
     */
    public HttpRequest setHttpProxy(String host, int port) {
        config.setHttpProxy(host, port);
        return this;
    }

    /**
     * 设置代理
     *
     * @param proxy 代理 {@link Proxy}
     * @return this
     */
    public HttpRequest setProxy(Proxy proxy) {
        config.setProxy(proxy);
        return this;
    }

    /**
     * 设置SSLSocketFactory<br>
     * 只针对HTTPS请求，如果不设置，使用默认的SSLSocketFactory<br>
     * 默认SSLSocketFactory为：SSLSocketFactoryBuilder.create().build();
     *
     * @param ssf SSLScketFactory
     * @return this
     */
    public HttpRequest setSSLSocketFactory(SSLSocketFactory ssf) {
        config.setSSLSocketFactory(ssf);
        return this;
    }

    /**
     * 设置HTTPS安全连接协议，只针对HTTPS请求，可以使用的协议包括：<br>
     * 此方法调用后{@link #setSSLSocketFactory(SSLSocketFactory)} 将被覆盖。
     *
     * <pre>
     * 1. TLSv1.2
     * 2. TLSv1.1
     * 3. SSLv3
     * ...
     * </pre>
     *
     * @param protocol 协议
     * @return this
     * @see SSLUtil#createSSLContext(String)
     * @see #setSSLSocketFactory(SSLSocketFactory)
     */
    public HttpRequest setSSLProtocol(String protocol) {
        config.setSSLProtocol(protocol);
        return this;
    }

    /**
     * 设置是否rest模式<br>
     * rest模式下get请求不会把参数附加到URL之后
     *
     * @param isRest 是否rest模式
     * @return this
     * @since 4.5.0
     */
    public HttpRequest setRest(boolean isRest) {
        this.isRest = isRest;
        return this;
    }

    /**
     * 采用流方式上传数据，无需本地缓存数据。<br>
     * HttpUrlConnection默认是将所有数据读到本地缓存，然后再发送给服务器，这样上传大文件时就会导致内存溢出。
     *
     * @param blockSize 块大小（bytes数），0或小于0表示不设置Chuncked模式
     * @return this
     * @since 4.6.5
     */
    public HttpRequest setChunkedStreamingMode(int blockSize) {
        config.setBlockSize(blockSize);
        return this;
    }

    /**
     * 设置拦截器，用于在请求前重新编辑请求
     *
     * @param interceptor 拦截器实现
     * @return this
     * @see #addRequestInterceptor(HttpInterceptor)
     * @since 5.7.16
     */
    public HttpRequest addInterceptor(HttpInterceptor<HttpRequest> interceptor) {
        return addRequestInterceptor(interceptor);
    }

    /**
     * 设置拦截器，用于在请求前重新编辑请求
     *
     * @param interceptor 拦截器实现
     * @return this
     * @since 5.8.0
     */
    public HttpRequest addRequestInterceptor(HttpInterceptor<HttpRequest> interceptor) {
        config.addRequestInterceptor(interceptor);
        return this;
    }

    /**
     * 设置拦截器，用于在请求前重新编辑请求
     *
     * @param interceptor 拦截器实现
     * @return this
     * @since 5.8.0
     */
    public HttpRequest addResponseInterceptor(HttpInterceptor<HttpResponse> interceptor) {
        config.addResponseInterceptor(interceptor);
        return this;
    }

    /**
     * 执行Reuqest请求
     *
     * @return this
     */
    public HttpResponse execute() {
        return this.execute(false);
    }

    /**
     * 异步请求<br>
     * 异步请求后获取的{@link HttpResponse} 为异步模式，执行完此方法后发送请求到服务器，但是并不立即读取响应内容。<br>
     * 此时保持Http连接不关闭，直调用获取内容方法为止。
     *
     * <p>
     * 一般执行完execute之后会把响应内容全部读出来放在一个 byte数组里，如果你响应的内容太多内存就爆了，此法是发送完请求不直接读响应内容，等有需要的时候读。
     *
     * @return 异步对象，使用get方法获取HttpResponse对象
     */
    public HttpResponse executeAsync() {
        return this.execute(true);
    }

    /**
     * 执行Reuqest请求
     *
     * @param isAsync 是否异步
     * @return this
     */
    public HttpResponse execute(boolean isAsync) {
        return doExecute(isAsync, config.requestInterceptors, config.responseInterceptors);
    }

    /**
     * 执行Request请求后，对响应内容后续处理<br>
     * 处理结束后关闭连接
     *
     * @param consumer 响应内容处理函数
     * @since 5.7.8
     */
    public void then(Consumer<HttpResponse> consumer) {
        try (final HttpResponse response = execute(true)) {
            consumer.accept(response);
        }
    }

    /**
     * 执行Request请求后，对响应内容后续处理<br>
     * 处理结束后关闭连接
     *
     * @param <T>      处理结果类型
     * @param function 响应内容处理函数
     * @return 处理结果
     * @since 5.8.5
     */
    public <T> T thenFunction(Function<HttpResponse, T> function) {
        try (final HttpResponse response = execute(true)) {
            return function.apply(response);
        }
    }

    /**
     * 简单验证，生成的头信息类似于：
     * <pre>
     * Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
     * </pre>
     *
     * @param username 用户名
     * @param password 密码
     * @return this
     */
    public HttpRequest basicAuth(String username, String password) {
        return auth(HttpUtil.buildBasicAuth(username, password, charset));
    }

    /**
     * 简单代理验证，生成的头信息类似于：
     * <pre>
     * Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
     * </pre>
     *
     * @param username 用户名
     * @param password 密码
     * @return this
     * @since 5.4.6
     */
    public HttpRequest basicProxyAuth(String username, String password) {
        return proxyAuth(HttpUtil.buildBasicAuth(username, password, charset));
    }

    /**
     * 令牌验证，生成的头类似于："Authorization: Bearer XXXXX"，一般用于JWT
     *
     * @param token 令牌内容
     * @return HttpRequest
     * @since 5.5.3
     */
    public HttpRequest bearerAuth(String token) {
        return auth("Bearer " + token);
    }

    /**
     * 验证，简单插入Authorization头
     *
     * @param content 验证内容
     * @return HttpRequest
     * @since 5.2.4
     */
    public HttpRequest auth(String content) {
        header(Header.AUTHORIZATION, content, true);
        return this;
    }

    /**
     * 验证，简单插入Authorization头
     *
     * @param content 验证内容
     * @return HttpRequest
     * @since 5.4.6
     */
    public HttpRequest proxyAuth(String content) {
        header(Header.PROXY_AUTHORIZATION, content, true);
        return this;
    }

    @Override
    public String toString() {
        final StringBuilder sb = StrUtil.builder();
        sb.append("Request Url: ").append(this.url.setCharset(this.charset)).append(StrUtil.CRLF);

        // header
        sb.append("Request Headers: ").append(StrUtil.CRLF);
        for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
            sb.append("    ").append(
                            entry.getKey()).append(": ").append(CollUtil.join(entry.getValue(), ","))
                    .append(StrUtil.CRLF);
        }

        // body
        sb.append("Request Body: ").append(StrUtil.CRLF);
        sb.append("    ").append(createBody()).append(StrUtil.CRLF);
        return sb.toString();
    }

    // ---------------------------------------------------------------- Private method start

    /**
     * 执行Reuqest请求
     *
     * @param isAsync              是否异步
     * @param requestInterceptors  请求拦截器列表
     * @param responseInterceptors 响应拦截器列表
     * @return this
     */
    private HttpResponse doExecute(boolean isAsync, HttpInterceptor.Chain<HttpRequest> requestInterceptors,
                                   HttpInterceptor.Chain<HttpResponse> responseInterceptors) {
        if (null != requestInterceptors) {
            for (HttpInterceptor<HttpRequest> interceptor : requestInterceptors) {
                interceptor.process(this);
            }
        }

        // 初始化URL
        urlWithParamIfGet();
        // 初始化 connection
        initConnection();
        // 发送请求
        send();

        // 手动实现重定向
        HttpResponse httpResponse = sendRedirectIfPossible(isAsync);

        // 获取响应
        if (null == httpResponse) {
            httpResponse = new HttpResponse(this.httpConnection, this.config, this.charset, isAsync, isIgnoreResponseBody());
        }

        // 拦截响应
        if (null != responseInterceptors) {
            for (HttpInterceptor<HttpResponse> interceptor : responseInterceptors) {
                interceptor.process(httpResponse);
            }
        }

        return httpResponse;
    }

    /**
     * 初始化网络连接
     */
    private void initConnection() {
        if (null != this.httpConnection) {
            // 执行下次请求时自动关闭上次请求（常用于转发）
            this.httpConnection.disconnectQuietly();
        }

        this.httpConnection = HttpConnection
                // issue#I50NHQ
                // 在生成正式URL前，设置自定义编码
                .create(this.url.setCharset(this.charset).toURL(this.urlHandler), config.proxy)//
                .setConnectTimeout(config.connectionTimeout)//
                .setReadTimeout(config.readTimeout)//
                .setMethod(this.method)//
                .setHttpsInfo(config.hostnameVerifier, config.ssf)//
                // 关闭JDK自动转发，采用手动转发方式
                .setInstanceFollowRedirects(false)
                // 流方式上传数据
                .setChunkedStreamingMode(config.blockSize)
                // issue#3462 自定义body长度
                .setFixedLengthStreamingMode(this.fixedContentLength)
                // 覆盖默认Header
                .header(this.headers, false);

        if (null != this.cookie) {
            // 当用户自定义Cookie时，全局Cookie自动失效
            this.httpConnection.setCookie(this.cookie);
        } else {
            // 读取全局Cookie信息并附带到请求中
            GlobalCookieManager.add(this.httpConnection);
        }

        // 是否禁用缓存
        if (config.isDisableCache) {
            this.httpConnection.disableCache();
        }
    }

    /**
     * 对于GET请求将参数加到URL中<br>
     * 此处不对URL中的特殊字符做单独编码<br>
     * 对于非rest的GET请求，且处于重定向时，参数丢弃
     */
    private void urlWithParamIfGet() {
        if (Method.GET.equals(method) && false == this.isRest && this.redirectCount <= 0) {
            UrlQuery query = this.url.getQuery();
            if (null == query) {
                query = new UrlQuery();
                this.url.setQuery(query);
            }

            // 优先使用body形式的参数，不存在使用form
            if (null != this.body) {
                query.parse(StrUtil.str(this.body.readBytes(), this.charset), this.charset);
            } else {
                query.addAll(this.form);
            }
        }
    }

    /**
     * 调用转发，如果需要转发返回转发结果，否则返回{@code null}
     *
     * @param isAsync 是否异步
     * @return {@link HttpResponse}，无转发返回 {@code null}
     */
    private HttpResponse sendRedirectIfPossible(boolean isAsync) {
        // 手动实现重定向
        if (config.maxRedirectCount > 0) {
            final int responseCode;
            try {
                responseCode = httpConnection.responseCode();
            } catch (IOException e) {
                // 错误时静默关闭连接
                this.httpConnection.disconnectQuietly();
                throw new HttpException(e);
            }
            // 支持自动重定向时处理cookie
            // https://github.com/dromara/hutool/issues/2960
            if (config.followRedirectsCookie) {
                GlobalCookieManager.store(httpConnection);
            }
            if (responseCode != HttpURLConnection.HTTP_OK) {
                if (HttpStatus.isRedirected(responseCode)) {
                    final UrlBuilder redirectUrl;
                    String location = httpConnection.header(Header.LOCATION);
                    if (false == HttpUtil.isHttp(location) && false == HttpUtil.isHttps(location)) {
                        // issue#I5TPSY, location可能为相对路径
                        if (false == location.startsWith("/")) {
                            location = StrUtil.addSuffixIfNot(this.url.getPathStr(), "/") + location;
                        }

                        // issue#3265, 相对路径中可能存在参数，单独处理参数
                        final String query;
                        final List<String> split = StrUtil.split(location, '?', 2);
                        if (split.size() == 2) {
                            // 存在参数
                            location = split.get(0);
                            query = split.get(1);
                        } else {
                            query = null;
                        }
                        redirectUrl = UrlBuilder.of(this.url.getScheme(), this.url.getHost(), this.url.getPort()
                                , location, query, null, this.charset);
                    } else {
                        redirectUrl = UrlBuilder.ofHttpWithoutEncode(location);
                    }
                    setUrl(redirectUrl);
                    // https://www.rfc-editor.org/rfc/rfc7231#section-6.4.7
                    // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Redirections
                    // 307方法和消息主体都不发生变化。
                    if (HttpStatus.HTTP_TEMP_REDIRECT != responseCode) {
                        // 重定向默认使用GET
                        method(Method.GET);
                    }
                    if (redirectCount < config.maxRedirectCount) {
                        redirectCount++;
                        // 重定向可选是否走过滤器
                        return doExecute(isAsync, config.interceptorOnRedirect ? config.requestInterceptors : null,
                                config.interceptorOnRedirect ? config.responseInterceptors : null);
                    }
                }
            }
        }
        return null;
    }

    /**
     * 发送数据流
     *
     * @throws IORuntimeException IO异常
     */
    private void send() throws IORuntimeException {
        try {
            if (Method.POST.equals(this.method) //
                    || Method.PUT.equals(this.method) //
                    || Method.DELETE.equals(this.method) //
                    || this.isRest) {
                if (isMultipart()) {
                    sendMultipart(); // 文件上传表单
                } else {
                    sendFormUrlEncoded();// 普通表单
                }
            } else {
                this.httpConnection.connect();
            }
        } catch (IOException e) {
            // 异常时关闭连接
            this.httpConnection.disconnectQuietly();
            throw new IORuntimeException(e);
        }
    }

    /**
     * 发送普通表单<br>
     * 发送数据后自动关闭输出流
     *
     * @throws IOException IO异常
     */
    private void sendFormUrlEncoded() throws IOException {
        if (this.config.useDefaultContentTypeIfNull && StrUtil.isBlank(this.header(Header.CONTENT_TYPE))) {
            // 如果未自定义Content-Type，使用默认的application/x-www-form-urlencoded
            this.httpConnection.header(Header.CONTENT_TYPE, ContentType.FORM_URLENCODED.toString(this.charset), true);
        }

        // Write的时候会优先使用body中的内容，write时自动关闭OutputStream
        createBody().writeClose(this.httpConnection.getOutputStream());
    }

    /**
     * 创建body
     *
     * @return body
     */
    private RequestBody createBody() {
        // Write的时候会优先使用body中的内容，write时自动关闭OutputStream
        if (null != this.body) {
            return ResourceBody.create(this.body);
        } else {
            return FormUrlEncodedBody.create(this.form, this.charset);
        }
    }

    /**
     * 发送多组件请求（例如包含文件的表单）<br>
     * 发送数据后自动关闭输出流
     *
     * @throws IOException IO异常
     */
    private void sendMultipart() throws IOException {
        final RequestBody body;
        // issue#3158，当用户自定义为multipart同时传入body，则不做单独处理
        if (null == form && null != this.body) {
            body = ResourceBody.create(this.body);
        } else {
            final MultipartBody multipartBody = MultipartBody.create(this.form, this.charset);
            //设置表单类型为Multipart（文件上传）
            this.httpConnection.header(Header.CONTENT_TYPE, multipartBody.getContentType(), true);
            body = multipartBody;
        }

        body.writeClose(this.httpConnection.getOutputStream());
    }

    /**
     * 是否忽略读取响应body部分<br>
     * HEAD、CONNECT、TRACE方法将不读取响应体
     *
     * @return 是否需要忽略响应body部分
     * @since 3.1.2
     */
    private boolean isIgnoreResponseBody() {
        return Method.HEAD == this.method //
                || Method.CONNECT == this.method //
                || Method.TRACE == this.method;
    }

    /**
     * 判断是否为multipart/form-data表单，条件如下：
     *
     * <pre>
     *     1. 存在资源对象（fileForm非空）
     *     2. 用户自定义头为multipart/form-data开头
     * </pre>
     *
     * @return 是否为multipart/form-data表单
     * @since 5.3.5
     */
    private boolean isMultipart() {
        if (this.isMultiPart) {
            return true;
        }

        final String contentType = header(Header.CONTENT_TYPE);
        return StrUtil.isNotEmpty(contentType) &&
                contentType.startsWith(ContentType.MULTIPART.getValue());
    }

    /**
     * 将参数加入到form中，如果form为空，新建之。
     *
     * @param name  表单属性名
     * @param value 属性值
     * @return this
     */
    private HttpRequest putToForm(String name, Object value) {
        if (null == name || null == value) {
            return this;
        }
        if (null == this.form) {
            this.form = new TableMap<>(16);
        }
        this.form.put(name, value);
        return this;
    }
    // ---------------------------------------------------------------- Private method end

}
